﻿/*************************************************************************
Diagrams library
Copyright (c) 2010 Pavel Torgashov.

>>> LICENSE >>>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation (www.fsf.org); either version 2 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

A copy of the GNU General Public License is available at
http://www.fsf.org/licensing/licenses

>>> END OF LICENSE >>>
*************************************************************************/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing.Drawing2D;
using System.Drawing;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Runtime.Serialization;

namespace Diagrams
{
    //диаграмма
    [Serializable]
    public class Diagram
    {
        //список фигур диаграммы
        public readonly List<Figure> figures = new List<Figure>();

        //сохранение диаграммы в файл
        public void Save(string fileName)
        {
            using (FileStream fs = new FileStream(fileName, FileMode.Create))
                new BinaryFormatter().Serialize(fs, this);
        }

        //чтение диаграммы из файла
        public static Diagram Load(string fileName)
        {
            using (FileStream fs = new FileStream(fileName, FileMode.Open))
                return (Diagram)new BinaryFormatter().Deserialize(fs);
        }

        public string FirstFigureName
        {
            get
            {
                SolidFigure firstFigure = figures.Find(fig => fig is SolidFigure && (fig as SolidFigure).IsFirst == true) as SolidFigure;
                return firstFigure != null?firstFigure.text:string.Empty;
            }
        }

        public string LastFigureName
        {
            get
            {
                SolidFigure lastFigure = figures.Find(fig => fig is SolidFigure && (fig as SolidFigure).IsLast == true) as SolidFigure;
                return lastFigure != null ? lastFigure.text : string.Empty;
            }
        }

        public void SetFistFigure(Figure selectedFigure)
        {
            if (selectedFigure != null && (selectedFigure is SolidFigure))
            {
                SolidFigure firstFigure = figures.Find(fig => fig is SolidFigure && (fig as SolidFigure).IsFirst == true) as SolidFigure;
                if (firstFigure != null)
                    firstFigure.IsFirst = false;

                SolidFigure figure = (selectedFigure as SolidFigure);
                figure.IsFirst = true;
                figure.IsLast = false;
            }
        }

        public void SetLastFigure(Figure selectedFigure)
        {
            if (selectedFigure != null && (selectedFigure is SolidFigure))
            {
                SolidFigure lastFigure = figures.Find(fig => fig is SolidFigure && (fig as SolidFigure).IsLast == true) as SolidFigure;
                if (lastFigure != null)
                    lastFigure.IsLast = false;

                SolidFigure figure = (selectedFigure as SolidFigure);
                figure.IsFirst = false;
                figure.IsLast = true;
            }
        }

    }

    //фигура
    [Serializable]
    public abstract class Figure
    {
        //линии фигуры
        readonly SerializableGraphicsPath serializablePath = new SerializableGraphicsPath();
        protected GraphicsPath Path { get { return serializablePath.path; } }
        //карандаш отрисовки линий
        public static Pen pen = Pens.Black;

        //точка находится внутри фигуры?
        public abstract bool IsInsidePoint(Point p);

        //отрисовка фигуры
        public abstract void Draw(Graphics gr);

        //получение маркеров
        public abstract List<Marker> CreateMarkers(Diagram diagram);
    }

    //многоугольник с текстом внутри
    [Serializable]
    public abstract class SolidFigure: Figure
    {
        //размер новой фигуры, по умолчанию
        protected static int defaultSize = 40;
        //заливка фигуры
        [NonSerialized]
        public Brush brush = Brushes.White;
        //местоположение центра фигуры
        public Point location;
        //прямоугольник, в котором расположен текст
        protected RectangleF textRect;
        //текст
        public string text = null;
        //настройки вывода текста
        protected virtual StringFormat StringFormat
        {
            get {
                StringFormat stringFormat = new StringFormat();
                stringFormat.Alignment = StringAlignment.Center;
                stringFormat.LineAlignment = StringAlignment.Center;
                return stringFormat;
            }
        }

        public bool IsFirst { get; set; }

        public bool IsLast { get; set; }

        //находится ли точка внутри контура?
        public override bool IsInsidePoint(Point p)
        {
            return Path.IsVisible(p.X - location.X, p.Y - location.Y);
        }
        
        //прямоугольник вокруг фигуры (в абсолютных координатах)
        public RectangleF Bounds
        {
            get
            {
                RectangleF bounds = Path.GetBounds();
                return new RectangleF(bounds.Left + location.X, bounds.Top + location.Y, bounds.Width, bounds.Height);
            }
        }

        //прямоугольник текста (в абсолютных координатах)
        public Rectangle TextBounds
        {
            get
            {
                return new Rectangle((int)textRect.Left + location.X, (int)textRect.Top + location.Y, (int)textRect.Width, (int)textRect.Height);
            }
        }

        //размер прямоугольника вокруг фигуры
        public SizeF Size
        {
            get { return Path.GetBounds().Size; }
            set
            {
                SizeF oldSize = Path.GetBounds().Size;
                SizeF newSize = new SizeF(Math.Max(1, value.Width), Math.Max(1, value.Height));
                //коэффициент шкалировани по x
                float kx = newSize.Width / oldSize.Width;
                //коэффициент шкалировани по y
                float ky = newSize.Height / oldSize.Height;
                Scale(kx, ky);
            }
        }

        //изменение масштаба фигуры
        public void Scale(float scaleX, float scaleY)
        {
            //масштабируем линии
            Matrix m = new Matrix();
            m.Scale(scaleX, scaleY);
            Path.Transform(m);
            //масштабируем прямоугльник текста
            textRect = new RectangleF(textRect.Left * scaleX, textRect.Top * scaleY, textRect.Width * scaleX, textRect.Height * scaleY);
        }

        //сдвиг местоположения фигуры
        public virtual void Offset(int dx, int dy)
        {
            location.Offset(dx, dy);
            if(location.X < 0)
                location.X = 0;
            if (location.Y < 0)
                location.Y = 0;
        }

        //отрисовка фигуры
        public override void Draw(Graphics gr)
        {
            gr.TranslateTransform(location.X, location.Y);

            if (IsFirst)
            {
                brush = Brushes.Green;
            }
            else if (IsLast)
            {
                brush = Brushes.Yellow;
            }
            else
            {
                brush = Brushes.White;
            }

            gr.FillPath(brush, Path);
            gr.DrawPath(pen, Path);
            if (!string.IsNullOrEmpty(text))
                gr.DrawString(text, SystemFonts.DefaultFont, Brushes.Black, textRect, StringFormat);
            gr.ResetTransform();
        }

        //создание маркера для изменения размера
        public override List<Marker> CreateMarkers(Diagram diagram)
        {
            List<Marker> markers = new List<Marker>();
            Marker m = new SizeMarker();
            m.targetFigure = this;
            markers.Add(m);

            return markers;
        }
    }

    //прямоугольник
    [Serializable]
    public class RectFigure : SolidFigure
    {
        public RectFigure()
        {
            Path.AddRectangle(new RectangleF(-defaultSize, -defaultSize/2, 2*defaultSize, defaultSize));
            textRect = new RectangleF(-defaultSize + 3, -defaultSize / 2 + 2, 2 * defaultSize - 6, defaultSize - 4);
        }
    }

    //скругленный прямоугольник
    [Serializable]
    public class RoundRectFigure : SolidFigure
    {
        public RoundRectFigure()
        {
            float diameter = 16f; 
            SizeF sizeF = new SizeF( diameter, diameter );
            RectangleF arc = new RectangleF( -defaultSize, -defaultSize/2, sizeF.Width, sizeF.Height );
            Path.AddArc( arc, 180, 90 );
            arc.X = defaultSize-diameter;
            Path.AddArc( arc, 270, 90 );
            arc.Y = defaultSize/2-diameter;
            Path.AddArc( arc, 0, 90 );
            arc.X = -defaultSize;
            Path.AddArc( arc, 90, 90 );
            Path.CloseFigure(); 

            textRect = new RectangleF(-defaultSize + 3, -defaultSize / 2 + 2, 2 * defaultSize - 6, defaultSize - 4);
        }
    }

    //ромб
    [Serializable]
    public class RhombFigure : SolidFigure
    {
        public RhombFigure()
        {
            Path.AddPolygon(new PointF[]{
                new PointF(-defaultSize, 0),
                new PointF(0, -defaultSize/2),
                new PointF(defaultSize, 0),
                new PointF(0, defaultSize/2)
            });
            textRect = new RectangleF(-defaultSize/2, -defaultSize / 4, defaultSize, defaultSize/2);
        }
    }

    //паралелограмм
    [Serializable]
    public class ParalelogrammFigure : SolidFigure
    {
        public ParalelogrammFigure()
        {
            float shift = 8f;
            Path.AddPolygon(new PointF[]{
                new PointF(-defaultSize + shift/2, -defaultSize/2),
                new PointF(defaultSize + shift/2, -defaultSize/2),
                new PointF(defaultSize - shift/2, defaultSize/2),
                new PointF(-defaultSize - shift/2, defaultSize/2),
            });
            textRect = new RectangleF(-defaultSize + shift / 2, -defaultSize / 2 + 2, 2 * defaultSize - shift, defaultSize - 4);
        }
    }

    //эллипс
    [Serializable]
    public class EllipseFigure : SolidFigure
    {
        public EllipseFigure()
        {
            Path.AddEllipse(new RectangleF(-defaultSize, -defaultSize/2, defaultSize*2, defaultSize));
            textRect = new RectangleF(-defaultSize / 1.4f, -defaultSize / 2 / 1.4f , 2 * defaultSize / 1.4f, defaultSize / 1.4f);
        }
    }

    //стопка прямоугольников
    [Serializable]
    public class StackFigure : SolidFigure
    {
        public StackFigure()
        {
            float shift = 4f;
            Path.AddRectangle(new RectangleF(-defaultSize, -defaultSize / 2, defaultSize * 2, defaultSize));
            Path.AddLines(new PointF[]{
                new PointF(-defaultSize + shift, defaultSize / 2),
                new PointF(-defaultSize + shift, defaultSize / 2 + shift),
                new PointF(defaultSize + shift, defaultSize / 2 + shift),
                new PointF(defaultSize + shift, -defaultSize / 2 + shift),
                new PointF(defaultSize, -defaultSize / 2 + shift),
                new PointF(defaultSize, defaultSize / 2)
            });

            textRect = new RectangleF(-defaultSize + 3, -defaultSize / 2 + 2, 2 * defaultSize - 6, defaultSize - 4);
        }
    }

    //рамка
    [Serializable]
    public class FrameFigure : SolidFigure
    {
        static Pen clickPen = new Pen(Color.Transparent, 3);

        protected override StringFormat StringFormat
        {
            get
            {
                StringFormat stringFormat = new StringFormat();
                stringFormat.Alignment = StringAlignment.Near;
                stringFormat.LineAlignment = StringAlignment.Near;
                return stringFormat;
            }
        }

        public FrameFigure()
        {
            Path.AddRectangle(new RectangleF(0, -defaultSize, defaultSize * 4, defaultSize*2));
            textRect = new RectangleF(0, -defaultSize, defaultSize * 4, defaultSize * 2);
        }

        public override bool IsInsidePoint(Point p)
        {
            return Path.IsOutlineVisible(p.X - location.X, p.Y - location.Y, clickPen);
        }

        public override void Draw(Graphics gr)
        {
            gr.TranslateTransform(location.X, location.Y);
            gr.DrawPath(pen, Path);
            if (!string.IsNullOrEmpty(text))
                gr.DrawString(text, SystemFonts.DefaultFont, Brushes.Black, textRect, StringFormat.GenericDefault);
            gr.ResetTransform();
        }
    }


    //соединительная линия
    [Serializable]
    public class LineFigure : Figure
    {
        public SolidFigure From;
        public SolidFigure To;
        static Pen clickPen = new Pen(Color.Transparent, 3);

        //размер новой фигуры, по умолчанию
        protected static int defaultSize = 40;
        //прямоугольник, в котором расположен текст
        protected RectangleF textRect = new RectangleF(-defaultSize + 3, -defaultSize / 2 + 2, 2 * defaultSize - 6, defaultSize - 4);
        //текст
        public string text = "1";//null;
        //настройки вывода текста
        protected virtual StringFormat StringFormat
        {
            get
            {
                StringFormat stringFormat = new StringFormat();
                stringFormat.Alignment = StringAlignment.Center;
                stringFormat.LineAlignment = StringAlignment.Center;
                return stringFormat;
            }
        }

        //прямоугольник текста (в абсолютных координатах)
        public Rectangle TextBounds
        {
            get
            {
                return new Rectangle((int)textRect.Left + (From.location.X + To.location.X) / 2, (int)textRect.Top + (From.location.Y + To.location.Y) / 2, (int)textRect.Width, (int)textRect.Height);
            }
        }

        public override void Draw(Graphics gr)
        {
            if (From == null || To == null)
                return;

            RecalcPath();
            gr.DrawPath(pen, Path);
            if (!string.IsNullOrEmpty(text))
                gr.DrawString(text, SystemFonts.DefaultFont, Brushes.Black, TextBounds, StringFormat);
        }

        public override bool IsInsidePoint(Point p)
        {
            if (From == null || To == null)
                return false;

            RecalcPath();
            return Path.IsOutlineVisible(p, clickPen);
        }

        protected virtual void RecalcPath()
        {
            PointF[] points = null;
            if(Path.PointCount>0)
                points = Path.PathPoints;
            if(Path.PointCount!=2 || points[0]!=From.location || points[1]!=To.location)
            {
                Path.Reset();
                Path.AddLine(From.location, To.location);
            }
        }

        public override List<Marker> CreateMarkers(Diagram diagram)
        {
            List<Marker> markers = new List<Marker>();
            EndLineMarker m1 = new EndLineMarker(diagram, 0);
            m1.targetFigure = this;
            EndLineMarker m2 = new EndLineMarker(diagram, 1);
            m2.targetFigure = this;

            markers.Add(m1);
            markers.Add(m2);

            return markers;
        }
    }

    //линия с "переломом"
    [Serializable]
    public class LedgeLineFigure : LineFigure
    {
        //координата X точки "перелома"
        internal float ledgePositionX = -1;

        protected override void RecalcPath()
        {
            PointF[] points = null;

            if (ledgePositionX < 0)
                ledgePositionX = (From.location.X + To.location.X) / 2;

            if (Path.PointCount > 0)
                points = Path.PathPoints;
            if (Path.PointCount != 4 || points[0] != From.location || points[3] != To.location ||
                points[1].X!=ledgePositionX)
            {
                Path.Reset();
                Path.AddLines(new PointF[]{
                    From.location,
                    new PointF(ledgePositionX, From.location.Y),
                    new PointF(ledgePositionX, To.location.Y),
                    To.location
                    });
            }
        }

        public override List<Marker> CreateMarkers(Diagram diagram)
        {
            RecalcPath();
            List<Marker> markers = new List<Marker>();
            EndLineMarker m1 = new EndLineMarker(diagram, 0);
            m1.targetFigure = this;
            EndLineMarker m2 = new EndLineMarker(diagram, 1);
            m2.targetFigure = this;
            LedgeMarker m3 = new LedgeMarker();
            m3.targetFigure = this;
            m3.UpdateLocation();

            markers.Add(m1);
            markers.Add(m2);
            markers.Add(m3);

            return markers;
        }
    }

    //сериализуемая обертка над GraphicsPath
    [Serializable]
    public class SerializableGraphicsPath: ISerializable
    {
        public GraphicsPath path = new GraphicsPath();

        public SerializableGraphicsPath()
        {
        }

        private SerializableGraphicsPath(SerializationInfo info, StreamingContext context)
        {
            if (info.MemberCount > 0)
            {
                PointF[] points = (PointF[])info.GetValue("p", typeof(PointF[]));
                byte[] types = (byte[])info.GetValue("t", typeof(byte[]));
                path = new GraphicsPath(points, types);
            }
            else
                path = new GraphicsPath();
        }

        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            if (path.PointCount > 0)
            {
                info.AddValue("p", path.PathPoints);
                info.AddValue("t", path.PathTypes);
            }
        }
    }


    [Serializable]
    public abstract class Marker : SolidFigure
    {
        protected static new int defaultSize = 2;
        public Figure targetFigure;

        public override bool IsInsidePoint(Point p)
        {
            if (p.X < location.X - defaultSize || p.X > location.X + defaultSize)
                return false;
            if (p.Y < location.Y - defaultSize || p.Y > location.Y + defaultSize)
                return false;

            return true;
        }

        public override void Draw(Graphics gr)
        {
            gr.DrawRectangle(Pens.Black, location.X - defaultSize, location.Y - defaultSize, defaultSize * 2, defaultSize * 2);
            gr.FillRectangle(Brushes.Red, location.X - defaultSize, location.Y - defaultSize, defaultSize * 2, defaultSize * 2);
        }

        public abstract void UpdateLocation();
    }

    public class SizeMarker : Marker
    {
        public override void UpdateLocation()
        {
            RectangleF bounds = (targetFigure as SolidFigure).Bounds;
            location = new Point((int)Math.Round(bounds.Right) + defaultSize / 2, (int)Math.Round(bounds.Bottom) + defaultSize / 2);
        }

        public override void Offset(int dx, int dy)
        {
            base.Offset(dx, dy);
            (targetFigure as SolidFigure).Size =
                SizeF.Add((targetFigure as SolidFigure).Size, new SizeF(dx * 2, dy * 2));
        }
    }

    [Serializable]
    public class EndLineMarker : Marker
    {
        int pointIndex;
        Diagram diagram;

        public EndLineMarker(Diagram diagram, int pointIndex)
        {
            this.diagram = diagram;
            this.pointIndex = pointIndex;
        }

        public override void UpdateLocation()
        {
            LineFigure line = (targetFigure as LineFigure);
            if (line.From == null || line.To == null)
                return;//не обновляем маркеры оторванных концов
            //фигура, с которой связана линия
            SolidFigure figure = pointIndex == 0 ? line.From : line.To;
            location = figure.location;
        }

        public override void Offset(int dx, int dy)
        {
            base.Offset(dx, dy);

            //ищем фигуру под маркером
            SolidFigure figure = null;
            for (int i = diagram.figures.Count - 1; i >= 0; i--)
                if (diagram.figures[i] is SolidFigure && diagram.figures[i].IsInsidePoint(location))
                {
                    figure = (SolidFigure)diagram.figures[i];
                    break;
                }

            LineFigure line = (targetFigure as LineFigure);
            if (figure == null)
                figure = this;//если под маркером нет фигуры, то просто коннектим линию к самому маркеру

            //не позволяем конектится самому к себе
            if (line.From == figure || line.To == figure)
                return;
            //обновляем конекторы линии
            if (pointIndex == 0)
                line.From = figure;
            else
                line.To = figure;

        }
    }

    [Serializable]
    public class LedgeMarker : Marker
    {
        public override void UpdateLocation()
        {
            LedgeLineFigure line = (targetFigure as LedgeLineFigure);
            if (line.From == null || line.To == null)
                return;//не обновляем маркеры оторванных концов
            //фигура, с которой связана линия
            location = new Point((int)line.ledgePositionX, (int)(line.From.location.Y + line.To.location.Y) / 2);
        }

        public override void Offset(int dx, int dy)
        {
            base.Offset(dx, 0);
            (targetFigure as LedgeLineFigure).ledgePositionX += dx;
        }
    }
}
